package com.example.mymessagequeue.mqserver.core;


import com.example.mymessagequeue.common.MqException;

/**
 * 使用这个类, 来实现交换机的转发规则。
 * 同时也借助这个类来验证 bindingKey 是否合法
 */
public class Router {

    /**
     * 判定 bindingKey 是否合法
     * bindingKey 构造规则:
     * 1. 数字，字母，下划线
     * 2. 使用 . 分割成若干部分
     * 3. 允许存在 * 和 # 作为通配符, 但是通配符只能作为一个独立的分段
     * @param bindingKey
     * @return
     */
    public boolean checkBindingKey(String bindingKey) {
        if (bindingKey.length() == 0) {
            // 空字符串. 合法的情况，比如在使用 direct / fanout 交换机时, bindingKey 是用不上的。
            return true;
        }

        for (int i = 0; i < bindingKey.length(); i++) {
            char ch = bindingKey.charAt(i);
            // 判断该字符是否是大写字母
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            // 判定该字母是否是小写字母
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            // 判定该字母是否是阿拉伯数字
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            // 判定是否是 _ 或者 .
            if (ch == '_' || ch == '.' || ch == '*' || ch == '#') {
                continue;
            }
            return false;
        }
        // 检查 * 或者 # 是否是独立的部分
        // aaa.*.b 合法, a.a*.b 非法
        // '.' 在正则表达式中, 是一个特殊的符号, 此处把 . 当作原始文本进行匹配, 要想使用 . 原始的文本, 就需要使用 \. 表示, 又因为当前是在Java字符串中写入 \. 这样的文本, 而 \ 又是特殊字符, 就需要 \\ 先对反斜杠转移, 此时才能够真正录入
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
            // 检查 word 的长度如果 > 1 并且包含了 * 或 # 就是非法的格式了.
            if (word.length() > 1 && (word.contains("*") || word.contains("#"))) {
                return false;
            }
        }
        // 约定一下通配符之间的相邻关系(人为约定， 前面三种情况实现匹配逻辑会很繁琐，功能性提升不大)
        // 1. aaa.#.#.bbb -> 非法
        // 2. aaa.#.*.bbb -> 非法
        // 3. aaa.*.#.bbb -> 非法
        // 4. aaa.*.*.bbb -> 合法
        for (int i = 0; i < words.length - 1; i++) {
            // 连续两个 #.#
            if (words[i].equals("#") && words[i + i].equals("#")) {
                return false;
            }
            // #.*
            if (words[i].equals("#") && words[i + i].equals("*")) {
                return false;
            }
            // *.#
            if (words[i].equals("*") && words[i + i].equals("#")) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判定 routingKey 是否合法
     * routingKey 的构造规则：
     * 1. 数字，字母，下划线
     * 2. 使用 . 分割成若干部分
     * @param routingKey
     * @return
     */
    public boolean checkRoutingKey(String routingKey) {
        if (routingKey.length() == 0) {
            // 空字符串. 合法的情况，比如在使用 fanout 交换机的时候，routingKey 用不上
            return true;
        }

        for (int i = 0; i < routingKey.length(); i++) {
            char ch = routingKey.charAt(i);
            // 判断该字符是否是大写字母
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            // 判定该字母是否是小写字母
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            // 判定该字母是否是阿拉伯数字
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            // 判定是否是 _ 或者 .
            if (ch == '_' || ch == '.') {
                continue;
            }
            return false;
        }
        return true;
    }

    /**
     * 判断当前消息能否转发给当前绑定的队列
     * @param exchangeType
     * @param binding
     * @param message
     * @return
     */
    public boolean route(ExchangeType exchangeType, Binding binding, Message message) throws MqException {
        // 根据不同的 exchangeType 使用不同的判定转发规则
        if (exchangeType == ExchangeType.FANOUT) {
            // 如果是 fanout 则该交换机上绑定的所有队列都要转发
            return true;
        } else if (exchangeType == ExchangeType.TOPIC) {
            // 如果是 topic 主题交换机，规则就要更复杂
            return routeTopic(binding, message);

        } else {
            // 其他情况是不应该存在的
            throw new MqException("[Router] 交换机类型非法! exchangeType=" + exchangeType);
        }
    }

    /**
     * 判断 routingKey 是否和 bindingKey 匹配
     * @param binding
     * @param message
     * @return
     */
    private boolean routeTopic(Binding binding, Message message) {
        // 切分两个key
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");

        // 引入两个下标
        int bindingIndex = 0;
        int routingIndex = 0;

        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if (bindingTokens[bindingIndex].equals("*")) {
                // 遇到 *, 直接进入下一轮, * 号可以匹配任意一个部分
                bindingIndex++;
                routingIndex++;
            } else if (bindingTokens[bindingIndex].equals("#")) {
                // 遇到 #, 需要先判断是否有下一个部分
                bindingIndex++;
                if (bindingIndex == bindingTokens.length) {
                    // 该 # 后面没有元素了
                    return true;
                } else {
                    // # 后面仍有元素, 拿着当前 bindingIndex 指向的内容, 去 routingKey 中找到对应的位置
                    // findNextMatch 这个方法用来查找该部分在 routingKey 中的位置, 返回该下标, 没找到返回 -1
                    routingIndex = findNextMatch(routingTokens, routingIndex, bindingTokens[bindingIndex]);
                    if (routingIndex == -1) {
                        return false;
                    }
                    // 匹配到了相同的元素, 则继续往后匹配
                    bindingIndex++;
                    routingIndex++;
                }
            } else {
                // 如果是普通字符串就判断是否相等
                if (!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    // 不相等直接返回false
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }

        // 双方同时到达末尾则匹配成功
        return bindingIndex == bindingTokens.length && routingIndex == routingTokens.length;
    }

    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if (routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }
}
